%!Tex Program = xelatex
%\documentclass[a4paper]{article}
\documentclass[a4paper]{ctexart}
\usepackage{xltxtra}
\usepackage{listings}
\usepackage{amsmath}
%\setmainfont[Mapping=tex-text]{AR PL UMing CN:style=Light}
%\setmainfont[Mapping=tex-text]{AR PL UKai CN:style=Book}
%\setmainfont[Mapping=tex-text]{WenQuanYi Zen Hei:style=Regular}
%\setmainfont[Mapping=tex-text]{WenQuanYi Zen Hei Sharp:style=Regular}
%\setmainfont[Mapping=tex-text]{AR PL KaitiM GB:style=Regular} 
%\setmainfont[Mapping=tex-text]{AR PL SungtiL GB:style=Regular} 
%\setmainfont[Mapping=tex-text]{WenQuanYi Zen Hei Mono:style=Regular} 
\usepackage{xcolor}
\lstset{
 linewidth=1.1\textwidth,
 basicstyle=\small\ttfamily, % 设置字体族
  backgroundcolor=\color[RGB]{245,245,244},
 breaklines=true, % 自动换行
    columns=flexible,
     tabsize=4,
      escapeinside={\%*}{*)}, 
    numbers=left, % 显示行号在左边
    numbersep=2.5em, % 设置行号的具体位置
    numberstyle=\tiny, % 缩小行号
    frame=single, % 边框
    framesep=1em, % 设置代码与边框的距离
    extendedchars=false, %解决代码跨页时，章节标题，页眉等汉字不显示的问题  
    xleftmargin=2.5em,xrightmargin=2.5em, aboveskip=1em, %设置边距  
    columns=fullflexible,
    captionpos=b,   
}


\title{第三讲：Linux 环境和 Shell 编程}
\author{王何宇}
\date{}
\begin{document}
\maketitle
\pagestyle{plain}

\section{重定向、管道}

Shell 中一切都是文件，包括标准输入输出。比如 \verb|ls -l| 的输出结果默认是输出到 \verb|stdout|，
也就是标准输出设备，但你可以用重定向操作符将其重定向为一个文件，比如：
\begin{lstlisting}[language=sh]
ls -l > output.txt
\end{lstlisting}

这个就是把 \verb|ls -l| 的输出结果保存在 \verb|output.txt| 中。 

操作符 \verb|>| 是重写一个新文件，而 \verb|>>| 是追加在原文件末尾。
尝试一下。

我们知道除了标准输出文件 \verb|stdout|，还有一个标准错误文件 \verb|stderr|，
这个往往和 \verb|stdout| 指向同一个输出设备。但可以通过重定向分离。
一般默认的输出是标准输出文件，而 2 则值标准错误文件。比如：
\begin{lstlisting}[language=sh]
kill -HUP 1234
\end{lstlisting}
删除进程 1234, 显示
\begin{lstlisting}[language=sh]
bash: kill: (1234) - No such process
\end{lstlisting}
因为一般不会正好有这个进程号。这里全部都是标准错误文件输出的，所以如果运行
\begin{lstlisting}[language=sh]
kill -HUP 1234 > output.txt
\end{lstlisting}
则得到一个空文本文件。要
\begin{lstlisting}[language=sh]
kill -HUP 1234 2> output.txt
\end{lstlisting}
才能截获错误信息到文本文件。如果我们想同时截获标准输出和错误输出并放在一个文件里，可以
\begin{lstlisting}[language=sh]
kill -HUP 1234 >killout.txt 2>&1
\end{lstlisting}
注意，\verb|2>&1| 之间不能有空格。如果输出中有很多垃圾信息你不想看，
可以将其重定向到一个垃圾桶：
\begin{lstlisting}[language=sh]
kill -l 1234 2>/dev/null
\end{lstlisting}

既然标准输出可以重定向，标准输入自然也可以
\begin{lstlisting}[language=sh]
ls ~ -lR > output.txt
\end{lstlisting}
先搞一个大一点的输出，把它丢给 \verb|more| 做输入，功能是把输入一屏幕一屏幕输出。
\begin{lstlisting}[language=sh]
more < output.txt
\end{lstlisting}
注意这里操作符反向了。

然后这里有一件很土的事情，就是这个
\verb|output.txt| 就是一个中转站，何必一定要存下来呢？就不能直接转移过去？
也就是把一个命令的输出当作另一个命令的输入。这个当然可以，就叫管道，
操作符是 \verb|||。
\begin{lstlisting}[language=sh]
ls ~ -lR | more
\end{lstlisting}
大家可能注意到我曾经在输出中只显示有关键字的部分，用的也是管道：
\begin{lstlisting}[language=sh]
ls ~ -lR | grep usr
\end{lstlisting}
管道和重定向可以根据需要多重连接。

\section{循环过程}

Shell 提供了表示循环和分支判定的结构，如：
\begin{lstlisting}[language=sh]
for file in *
do
if grep -l figure $file
then
ls $file -l
fi
done
\end{lstlisting}
这里
\begin{lstlisting}[language=sh]
for file in *
do
...something...
done
\end{lstlisting}
是一个循环结构，\verb|file| 是一个用户定义的变量，
\verb|*| 表示当前目录下所有文件，\verb|in| 表示遍历。
所以这段命令的意思就是遍历当前目录下所有文件，对每个文件重复做
\verb|...something...| 这件事。比如：
\begin{lstlisting}[language=sh]
for fAd in *
do
echo $fAd
done
\end{lstlisting}
就是遍历当前目录，将全部文件名都打印一遍。命令 \verb|echo| 是打印输出的意思。

\section{分支过程}
然后看内层是一个分支结构：
\begin{lstlisting}[language=sh]
if [ ...condition... ]
then
...do something...
fi
\end{lstlisting}
以上是一个标准的 \verb|if ... fi| 结构。但我们用的时候没有
\verb|...condition...|，类似 C 语言，只要 \verb|...condition...| 部分不为空，
我们就认为条件是真。所以这里的意思是
\begin{lstlisting}[language=sh]
if grep -l figure $file
then
ls $file -l
fi
\end{lstlisting}
只要
\begin{lstlisting}[language=sh]
grep -l figure $file
\end{lstlisting}
过滤的结果不为空，即之前遍历到的 \verb|$file| 这些文件里存在包含 \verb|figure|
单词的，就把它的详细信息列出来。你自己可以根据需要做自由的调整，比如：
\begin{lstlisting}[language=sh]
for file in *
do
if grep -l figure $file
then
cp $file ~/Documents
fi
done
\end{lstlisting}
这是什么意思？已经具有收集功能了。

\section{通配符（wildcard expansion）}
注意到这里 \verb|*| 是一个典型的通配符，意思是全部文件；类似的通配符还有：
\begin{itemize}
\item \verb|?|，任一单个字符，比如：\verb|lec0?|，那么
  \verb|lec01|，\verb|lec02| 和 \verb|lec0k| 都匹配；
\verb|[]|，括号中任一字母。比如：\verb|lec0[abc]|，那么
\verb|lec0a|，\verb|lec0b| 和 \verb|lec0c| 都匹配；
\item \verb|[^]| 或 \verb|[!]|, 不含括号内的任一字符，比如：\verb|lec0[!12]|，
  那么 \verb|lec01| 和 \verb|lec02| 都不匹配；
\item \verb|{}|，包含大括号内任一字串，比如：\verb|lec03{.log,.pdf}| 则匹配
  \verb|lec03.pdf| 或 \verb|lec03.log|。
\end{itemize}
以上通配符可自由组合，比如：\verb|*0?.{t??,a*}|。

然后 \verb|$| 操作符主要有两个意思，要么是表示将一个变量替换成变量值，如之前的 \verb|$file|；
也可以表示对命令的替换。比如：
\begin{lstlisting}[language=sh]
grep -l crazyfish *
\end{lstlisting}
表示列出目录下所有含有 \verb|crazyfish| 字符串的文件名，那么
\begin{lstlisting}[language=sh]
ls -l $(grep -l crazyfish *)
\end{lstlisting}
就表示进一步列出这些文件的详细信息。

\section{脚本代码 (script)}

现在我们可以用连续的命令加上流程控制加上通配符实现比较复杂的功能。
这些命令可以形成一个脚本，并用 bash 去运行。具体做法是依次写下命令，
然后在第一行写上 \verb|#!/bin/bash| 表示这个脚本用哪个 shell 执行。
下面是一个完整的脚本的例子：
\begin{lstlisting}[language=sh]
#!/bin/sh
# first
# This file looks through all the files in the current
# directory for the string POSIX, and then prints the names of
# those files to the standard output.
for file in *
do
if grep -q crazyfish $file 
then
echo $file 
fi
done
exit 0
\end{lstlisting}
将它存成一个文本文件，比如叫 \verb|test.sh|，然后将其设置成可执行，
然后就可以像运行一个命令一样运行它。实际上，它构成一个组合命令，或称批处理。
这里 \verb|grep -q| 表示安静地运行，或者说，不要实际输出，因为我们这里只要求
\verb|grep| 返回是否找到这样一个状态就行。

\section{变量}

既然可以写脚本了，变量自然很重要。这里变量直接使用就可以：
\begin{lstlisting}[language=sh]
salutation=Hello
echo $salutation
\end{lstlisting}
一赋值一输出。默认就是字符串，引不引号无所谓：
\begin{lstlisting}[language=sh]
salutation="Yes Dear"
echo $salutation
\end{lstlisting}
就算是给数字，还是字符串：
\begin{lstlisting}[language=sh]
salutation=7+5
echo $salutation
\end{lstlisting}

命令 \verb|read| 可以由用户给变量赋值：
\begin{lstlisting}[language=sh]
read salutation
echo $salutation
\end{lstlisting}

从上面的过程我们可以看到，在 shell 中，双引号和不加引号都是字符串，
而单引号和 \verb|\| 才是真正的引号：
\begin{lstlisting}[language=sh]
#!/bin/sh
myvar="Hi there"
echo $myvar
echo "$myvar"
echo '$myvar'
echo \$myvar
echo Enter some text
read myvar
echo '$myvar' now equals $myvar
exit 0
\end{lstlisting}
(参见：example/scripts/quoting.sh)

\section{环境变量}

除了我们自定义的变量，shell 还内置了很多环境变量，供我们和系统交互。
\begin{itemize}
\item \verb|$HOME|，用户目录；
\item \verb|$PATH|，执行程序的路径，放在这些路径下的可执行文件能够直接被运行；
\item \verb|$PS1|，命令行提示符，普通用户是 \verb|$|，超级用户是 \verb|#|；
\item \verb|$PS2|，第二提示符，比如等待输入状态，通常是 \verb|>|；
\item \verb|$IFS|，输入参数的间隔符，一般是一个空格，也可以是 TAB 或换行；
\end{itemize}
以上都可以用 \verb|echo| 输出查看。但是
\begin{lstlisting}[language=sh]
echo $PS1
\end{lstlisting}
会得到
\begin{lstlisting}[language=sh]
${debian_chroot:+($debian_chroot)}\u@\h:\w\$
\end{lstlisting}
这里转义字符 \verb|\u| 表示用户名，\verb|\h| 表示主机名，\verb|\w| 表示当前路径。
以下环境变量专门给 scripts 使用，
\begin{itemize}
\item \verb|$0|，脚本名；
\item \verb|$#|，传递给脚本的参数个数；
\item \verb|$$|，脚本的进程编号；
\item \verb|$1|，\verb|$2|，...，传递给脚本的第 1 个，第 2 个参数，...；
\item \verb|$*|，传递给脚本的全部参数，作为一个字符串；
\item \verb|$@|，传递给脚本的全部参数，有几个参数就是几个字符串；
\end{itemize}
测试脚本(try\_var.sh)
\begin{lstlisting}[language=sh]
#!/bin/bash

echo “The program $0 is now running”
echo “The first parameter was $1”
echo “The second parameter was $2”
echo “The third parameter was $3”
echo “The all parameters were $*“

echo “The parameter list was:“
for para in $@
do
echo $para
done

exit 0
\end{lstlisting}
体会一下 \verb|@*| 和 \verb|$@| 的区别。

\section{判断条件（condition）}

实际上，正经的 \verb|if| 后面应该跟一个条件判断，条件的写法是：
\begin{lstlisting}[language=sh]
if ...condition...
\end{lstlisting}
这里 \verb|...condition...| 的有两种表达，\verb|test| 或者 \verb|[  ]|，
后者方括号两端必须各有一个空格。

\begin{table}[p!]
\caption{字符串逻辑运算关系表}
\centering
\begin{tabular}{|c|c|}
\hline
字符串比较 & 结果 \\
\hline
string1 = string2 & True 如果字符串相等 \\
\hline
string1 != string2 & True 如果字符串不想等 \\
\hline
-n string & True 如果字符串非空 \\
\hline
-z string & True 如果字符串空 \\
\hline
\end{tabular}
\end{table}

\begin{table}[p!]
  
\caption{算术逻辑运算关系表}
\centering
\begin{tabular}{|c|c|}
\hline
算术表达式 & 结果 \\
\hline
expression1 -eq expression2 & True 如果表达式值相等\\
\hline
expression1 -ne expression2 & True 如果表达式值不想等\\
\hline
expression1 -gt expression2 & True 如果 expression1 大于 expression2\\
\hline
expression1 -ge expression2 & True 如果 expression1 大于等于 expression2\\
\hline
expression1 -lt expression2 & True 如果 expression1 小于 expression2\\
\hline
expression1 -le expression2 & True 如果 expression1 小于等于 expression2\\
\hline
! expression & True 如果 expression false, 或反之\\
\hline
\end{tabular}
\end{table}

\begin{table}[p!]
  \caption{文件逻辑运算关系表}
\centering
\begin{tabular}{|c|c|}
\hline
文件判断 & 结果 \\
\hline
-d file & True 如果 file 是目录\\
\hline
-e file & True 如果 file 存在\\
\hline
-f file & True 如果 file 是正经的文件\\
\hline
-g file & True 如果 file 设置了 group id\\
\hline
-r file & True 如果 file 可读\\
\hline
-s file & True 如果 file 大小不是零\\
\hline
-u file & True 如果 file 设置了 user id\\
\hline
-w file & True 如果 file 可写\\
\hline
-x file & True 如果 file 可执行\\
\hline
\end{tabular}
\end{table}

\section{控制结构（Control Structures）}

就和别的程序语言一样，Shell 编程也可以使用循环、分支等结构，也可以引入局部或全局变量，它让编程语言更加简洁，也更加像一个“编程”，而非普通的命令行的堆砌。

\subsection{条件：\textbf{if,elif} 语句}

一个“如果...否则..."这样的语句，写法为：
\begin{lstlisting}[language=sh]
if condition
then
    statements
else
    statements
fi
\end{lstlisting}

和 C 语言中的 \textbf{else if} 很类似的，Shell 中还有一个 \textbf{elif} 语法。\\
比如下测试脚本：

\begin{lstlisting}[language=sh]
#!/bin/bash

echo “Is it morning? Please answer yes or no”
read timeofday

if [ “$timeofday” = “yes“]
then
  echo “Good morning”
elif [ “$timeofday” = “no” ]；then
  echo “Good afternoon”
else
  echo “Sorry，$timeofday not recognized. Enter yes or no”
  exit 1
fi
exit 0
\end{lstlisting}

\textbf{注意}：使用 \texttt{"\$timeofday"} 才是对变量的完整引用，如果写成 texttt{\$timeofday}，则不会保留空格，特殊字符等。

\subsection{循环：\textbf{for,while,until}语句}

用法非常类似 python 中的 for 语句：
\begin{lstlisting}[language=sh]
for variable in values
do
    statements
done
\end{lstlisting}

这里的 values 可以是一个 list，也可以是简单的枚举，比如可以写成：
\begin{lstlisting}[language=sh]
for foo in bar fud 42
\end{lstlisting}

有了 for 语句之后，就可以很方便的执行 shell 常用的功能：目录扫描。
如下是一个简单的测试脚本，扫描当前目录所有名字为 f*.sh 的文件：

\begin{lstlisting}[language=sh]
#!/bin/sh

for file in $(ls f*.sh); do
  lpr $file
done
exit 0
\end{lstlisting}

while 语句的语法为：
\begin{lstlisting}[language=sh]
while condition do
  statements
done
\end{lstlisting}

其中 condition 是一个条件语句。

于 C 中的 "do...while" 类似的，shell 中可以使用 until 语句：
\begin{lstlisting}[language=sh]
until condition
do
  statements
done
\end{lstlisting}

比如如下是一个 alarm 脚本，当另一个用户(登录名通过命令行传递)登录时被触发：

\begin{lstlisting}[language=sh]
#!/bin/bash
until who | grep “$1” > /dev/null
do
sleep 60
done
# now ring the bell and announce the expected user.
echo -e ‘\a’
echo “**** $1 has just logged in ****“
exit 0
\end{lstlisting}

\subsection{分支：\textbf{case}语句}
用法如下，其功能与 C 中的 switch-case 类似：
\begin{lstlisting}[language=sh]
case variable in
pattern [ | pattern] ...) statements;;
pattern [ | pattern] ...) statements;;
...
esac
\end{lstlisting}

以下是一个例子：
\begin{lstlisting}[language=sh]
#!/bin/sh
echo “Is it morning? Please answer yes or no”
read timeofday
case “$timeofday” in
yes) echo “Good Morning”;;
no ) echo “Good Afternoon”;;
y ) echo “Good Morning”;;
n ) echo “Good Afternoon”;;
* ) echo “Sorry, answer not recognized”;;
esac
exit 0
\end{lstlisting}

你还可以把 case 中的项并在一起，相当于合并了若干个情况：
\begin{lstlisting}[language=sh]
case “$timeofday” in
yes | y | Yes | YES ) echo “Good Morning”;;
n* | N* ) echo “Good Afternoon”;;
* ) echo “Sorry, answer not recognized”;;
esac
\end{lstlisting}

\subsection{逻辑语句：\textbf{List}}

List 语法用于执行逻辑判断，主要有与和或两种。

\begin{itemize}
    \item And List
    \begin{lstlisting}[language=sh]
#!/bin/sh
touch file_one
rm -f file_two
if [ -f file_one ] && echo “hello” && [ -f file_two ] && echo “ there”
then
echo “in if”
else
echo “in else”
fi
exit 0
    \end{lstlisting}
    \item Or List
    \begin{lstlisting}[language=sh]
#!/bin/sh
rm -f file_one
if [ -f file_one ] || echo “hello” || echo “ there”
then
echo “in if”
else
echo “in else”
fi
exit 0
    \end{lstlisting}
\end{itemize}

如果您希望在只允许使用一条语句的地方使用多条语句，例如在AND或or中
列表中，可以通过将它们用大括号{}括起来形成语句块来实现。

    \begin{lstlisting}[language=sh]
get_confirm && {
grep -v “$cdcatnum” $tracks_file > $temp_file
cat $temp_file > $tracks_file
echo
add_record_tracks
}
    \end{lstlisting}

\section{函数（Functions）}

函数的作用和函数对于编程的意义就不多说了，在 Shell 中，函数的定义方式为：函数名()，而函数体写在之后，并用花括号包括起来。（就和 C 语言一样）
以下是一个函数的例子：

\begin{lstlisting}[language=sh]
#!/bin/sh
yes_or_no() {
echo “Is your name $* ?”
while true
do
echo -n “Enter yes or no: “
read x
case “$x” in
y | yes ) return 0;;
n | no ) return 1;;
* ) echo “Answer yes or no”
esac
done
}

echo “Original parameters are $*“
if yes_or_no “$1”
then
echo “Hi $1, nice name”
else
echo “Never mind"
fi
exit 0
\end{lstlisting}

\section{命令（Command）}

接下来我们介绍一下其他的命令语句。

\subsection{break}
用在 for，while，until 等循环语句中。用于结束当前循环。
\begin{lstlisting}[language=sh]
#!/bin/sh
rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4
for file in fred*
do
if [ -d “$file” ]; then
break;
fi
done

echo first directory starting fred was $file

rm -rf fred*
exit 0
\end{lstlisting}

\subsection{（:）命令}
冒号命令表示一个空命令。冒号还可以用作一个占位符，没有实际的操作，只是为了满足Shell语法的需要。比如如下语句：
\begin{lstlisting}[language=sh]
:${var:=value}
\end{lstlisting}
这个语句的作用是如果 var 未定义或者为空，那么将其设置为value，否则保持原样。这里如果没有冒号，Shell会尝试将"\$var"作为一个命令进行评估，而不是进行参数扩展。

冒号作为空命令的一个例子：
\begin{lstlisting}[language=sh]
#!/bin/sh
rm -f fred
if [ -f fred ]; then
:
else
echo file fred did not exist
fi
exit 0
\end{lstlisting}

\subsection{continue}
用在 for，while，until 等循环语句中。用于结束当前层循环，直接跳转到循环判断语句。
\begin{lstlisting}[language=sh]
#!/bin/sh
rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4

for file in fred*
do
if [ -d “$file” ]; then
echo “skipping directory $file”
continue
fi
echo file is $file
done
rm -rf fred*
exit 0
\end{lstlisting}

\subsection{(.) 命令}

dot（.）命令在当前shell中执行命令：
\[
\text{. ./shell\_script}
\]

通常情况下，当脚本执行外部命令或脚本时，会创建一个新的环境（子shell），在新环境中执行命令，然后环境被丢弃，只返回退出代码给父shell。然而，外部source命令和dot命令（另外两个同义词）在调用脚本的同一个shell中运行脚本中列出的命令。因为默认情况下，当执行shell脚本时会创建一个新的环境，脚本所做的环境变量更改将会丢失。然而，dot命令允许执行的脚本更改当前环境。这在您使用脚本作为包装器来设置后续执行其他命令的环境时非常有用。例如，当您同时在多个不同项目上工作时，您可能会发现需要使用不同的参数来调用命令，例如调用旧版本的编译器以维护旧程序。

在shell脚本中，dot命令的工作方式有点像C或C++中的\#include指令。虽然它不是字面上包含脚本，但它确实在当前上下文中执行命令，因此您可以使用它将变量和函数定义合并到脚本中。假设您有两个包含两个不同开发环境的环境设置的文件。要设置旧的经典命令环境classic\_set，您可以使用以下内容：

\begin{lstlisting}[language=sh]
#!/bin/sh
version=classic
PATH=/usr/local/old_bin:/usr/bin:/bin:.
PS1="classic> "
\end{lstlisting}

对于新的命令，请使用latest\_set：

\begin{lstlisting}[language=sh]
#!/bin/sh
version=latest
PATH=/usr/local/new_bin:/usr/bin:/bin:.
PS1="latest version> "
\end{lstlisting}

\subsection{echo}

\begin{lstlisting}[language=sh]
echo -n “string to output"
echo -e "string t output\c"
\end{lstlisting}

第二个选项，echo -e，确保启用反斜杠转义字符的解释，例如\textbackslash c用于抑制换行符，\textbackslash t用于输出制表符，\textbackslash n用于输出回车符。在旧版本的bash中，通常默认启用此功能，但是较新版本的bash通常默认不解释反斜杠转义字符。

\subsection{eval}
eval命令可以用于执行参数的求值。它作为shell的内建功能，通常不作为单独的命令存在。以下是一个简短的例子，引用自X/Open规范本身：

\begin{lstlisting}[language=sh]
foo=10
x=foo
y='$'$x
echo $y
\end{lstlisting}

这将输出\$foo。然而，

\begin{lstlisting}[language=sh]
foo=10
x=foo
eval y='$'$x
echo $y
\end{lstlisting}

\subsection{exec}

exec命令有两种不同的用法。它的典型用法是用不同的程序替换当前的shell。例如，在脚本中执行以下命令：
\begin{lstlisting}[language=sh]
exec wall "Thanks for all the fish"
\end{lstlisting}
这将用wall命令替换当前的shell。因为执行脚本的shell不再存在，所以exec之后的脚本中的行将不会被处理。

exec的第二种用法是修改当前的文件描述符：
\begin{lstlisting}[language=sh]
exec 3< afile
\end{lstlisting}
这将使文件描述符三以读取模式打开文件afile。

\subsection{exit n}

exit命令使脚本以退出码n的方式退出。如果您在任何交互式shell的命令提示符下使用它，将会注销您的登录。如果您允许脚本在没有指定退出状态的情况下退出，那么脚本中执行的最后一个命令的状态将作为返回值。为脚本提供一个退出码是一种良好的实践。

在shell脚本编程中，退出码0表示成功，而退出码1到125（包括1和125）是脚本可以使用的错误码。剩下的值具有保留的含义，如下表所示：

\begin{tabular}{|c|p{7.5cm}|}
\hline
\textbf{退出码} & \textbf{描述} \\
\hline
0 & 成功 \\
1-125 & 用户定义的错误码 \\
126 & 文件不可执行 \\
127 & 命令未找到 \\
128及以上 & 信号发生 \\
\hline
\end{tabular}

使用零作为成功的退出码可能对许多C或C++程序员来说有点不寻常。在脚本中的重要优势是，它们使您能够使用125个用户定义的错误码，而无需全局错误码变量。

以下是一个简单的示例，如果当前目录中存在名为.profile的文件，则返回成功：
\begin{lstlisting}[language=sh]
#!/bin/sh
if [ -f .profile ]; then
    exit 0
fi
exit 1
\end{lstlisting}

如果您喜欢简洁的脚本，可以将此脚本使用之前展示的组合AND和OR列表重写为一行：
\begin{lstlisting}[language=sh]
[ -f .profile ] && exit 0
\end{lstlisting}

\subsection{export}

export命令使其参数命名的变量在子shell中可用。默认情况下，shell中创建的变量在从该shell调用的进一步（子）shell中不可用。export命令使用其参数创建一个环境变量，可以被从当前程序调用的其他脚本和程序所看到。更具体地说，导出的变量形成了从shell派生的任何子进程中的环境变量。以下是两个脚本export1和export2的示例来说明这一点。

首先，列出export2脚本：
\begin{lstlisting}[language=sh]
#!/bin/sh
echo "$foo"
echo "$bar"
\end{lstlisting}

现在是export1脚本。在该脚本的末尾调用export2：
\begin{lstlisting}[language=sh]
#!/bin/sh
foo="The first meta-syntactic variable"
export bar="The second meta-syntactic variable"
export2
\end{lstlisting}

通过在export1脚本中使用export命令，变量"bar"将成为一个环境变量，并且在export2脚本中可以访问和使用。

\subsection{expr}
expr命令将其参数作为表达式进行求值。它最常用于以下形式的简单算术运算：
\[
\text{x=`expr \$x + 1`}
\]
expr命令非常强大，可以执行许多表达式求值。以下是一些主要的表达式求值及其描述：

\begin{tabular}{|l|p{8cm}|}
\hline
\textbf{表达式求值} & \textbf{描述} \\
\hline
expr1 | expr2 & 如果expr1非零，则为expr1，否则为expr2 \\
expr1 \& expr2 & 如果任一表达式为零，则为零，否则为expr1 \\
expr1 = expr2 & 相等 \\
expr1 > expr2 & 大于 \\
expr1 >= expr2 & 大于等于 \\
expr1 < expr2 & 小于 \\
expr1 <= expr2 & 小于等于 \\
expr1 != expr2 & 不等于 \\
expr1 + expr2 & 加法 \\
expr1 - expr2 & 减法 \\
expr1 * expr2 & 乘法 \\
expr1 / expr2 & 整数除法 \\
expr1 \% expr2 & 整数取模 \\
\hline
\end{tabular}

\subsection{printf}
printf命令仅适用于较新的shell。X/Open建议优先使用printf而不是echo来生成格式化输出，尽管很少有人遵循这个建议。
语法如下：
printf "格式字符串" 参数1 参数2 ...

格式字符串与C或C++中使用的格式字符串非常相似，但有一些限制。主要的限制是不支持浮点数，因为shell中的所有算术运算都是以整数进行的。格式字符串可以由任意组合的字面字符、转义序列和转换说明符组成。格式字符串中除了%和\之外的所有字符都会以字面形式出现在输出中。支持以下转义序列：
转换说明符相当复杂，因此我们在这里只列出常用的用法。更多细节可以在bash在线手册或printf页面的在线手册的第1节（man 1 printf）中找到（如果在第1节中找不到，请尝试第3节作为替代）。转换说明符由%字符和转换字符组成。主要的转换如下表所示：

\begin{tabular}{|l|l|}
\hline
\textbf{转换说明符} & \textbf{描述} \\
\hline
D & 输出一个十进制数 \\
C & 输出一个字符 \\
S & 输出一个字符串 \\
% & 输出%字符 \\
\hline
\end{tabular}

然后，使用格式字符串来解释剩余的参数并输出结果，如下面的示例所示：

\begin{lstlisting}[language=sh]
$ printf "%s\n" hello
hello
$ printf "%s %d\t%s" "Hi There" 15 people
Hi There 15 people
\end{lstlisting}

转义序列说明如下：

\begin{tabular}{|l|p{6cm}|}
\hline
\textbf{转义序列} & \textbf{描述} \\
\hline
\textbackslash\" & 双引号 \\
\textbackslash\textbackslash & 反斜杠字符 \\
\textbackslash a & 响铃（发出响声或蜂鸣声） \\
\textbackslash b & 退格字符 \\
\textbackslash c & 不再输出后续内容 \\
\textbackslash f & 换页符 \\
\textbackslash n & 换行符 \\
\textbackslash r & 回车符 \\
\textbackslash t & 制表符 \\
\textbackslash v & 垂直制表符 \\
\textbackslash ooo & 八进制值为ooo的单个字符 \\
\textbackslash xHH & 十六进制值为HH的单个字符 \\
\hline
\end{tabular}


\subsection{return}

return命令接受一个单独的数字参数，该参数可在调用函数的脚本中使用。如果未指定参数，则return默认为上一条命令的退出代码。

\subsection{set}

set命令用于为shell设置参数变量。它可以是在输出空格分隔值的命令中使用字段的有用方式。

假设您想在shell脚本中使用当前月份的名称。系统提供了一个date命令，其中包含月份作为字符串，但您需要将其与其他字段分开。您可以使用set命令和\$(...)结构的组合来执行date命令并返回结果（稍后将更详细地介绍）。date命令的输出将月份字符串作为其第二个参数：

\begin{lstlisting}[language=sh]
#!/bin/sh
echo the date is $(date)
set $(date)
echo The month is $2
exit 0
\end{lstlisting}

该程序将参数列表设置为date命令的输出，然后使用位置参数\$2来获取月份。

请注意，我们使用date命令作为一个简单的示例，以展示如何提取位置参数。由于date命令对语言环境敏感，实际上您会使用date +\%B来提取月份的名称。date命令还有许多其他的格式选项；请参阅手册页获取更多详细信息。

\subsection{shift}

shift命令将所有的参数变量向下移动一个位置，使得\$2变成\$1，\$3变成\$2，依此类推。\$1的先前值被丢弃，而\$0保持不变。如果在调用shift时指定了一个数值参数，则参数向后移动相应的位置。\$*、\$\@和\$\#这些变量也根据参数变量的新排列进行修改。

shift命令经常用于遍历传递给脚本的参数，如果您的脚本需要10个或更多的参数，则需要使用shift来访问第十个及以后的参数。

例如，您可以通过以下方式遍历所有的位置参数：

\begin{lstlisting}[language=sh]
#!/bin/sh
while [ "$1" != "" ]; do
    echo "$1"
    shift
done
\end{lstlisting}


\subsection{trap}

trap命令用于指定接收到信号时要执行的操作。在历史上，shell通常使用数字来表示信号，但新的脚本应该使用从\#include文件signal.h中获取的名称，省略了SIG前缀。要查看信号编号和关联的名称，您可以在命令提示符下直接输入trap -l。

trap命令接收要执行的操作，后面跟着要捕获的信号名称（或多个信号名称）：

\begin{lstlisting}[language=sh]
trap 命令 信号
\end{lstlisting}

请记住，脚本通常是从上到下解释的，所以您必须在希望保护的脚本部分之前指定trap命令。

要将trap条件重置为默认值，只需将命令指定为“-”。要忽略一个信号，请将命令设置为空字符串“''”。不带参数的trap命令会打印出当前的捕获列表和操作。

下表列出了X/Open标准中涵盖的更重要的信号（括号中是常规的信号编号）。更多详细信息可以在在线手册的第7节信号手册页（man 7 signal）中找到。

\begin{tabular}{|c|p{7.5cm}|}
\hline
\textbf{信号} & \textbf{描述} \\
\hline
HUP (1) & 断开连接；通常在终端离线或用户注销时发送 \\
INT (2) & 中断；通常通过按下Ctrl+C发送 \\
QUIT (3) & 退出；通常通过按下Ctrl+\textbackslash 发送 \\
ABRT (6) & 中止；通常在发生严重执行错误时发送 \\
ALRM (14) & 闹钟；通常用于处理超时 \\
TERM (15) & 终止；通常由系统在关闭时发送 \\
\hline
\end{tabular}

\subsection{unset}
unset命令用于从环境中删除变量或函数。但它不能删除由shell本身定义的只读变量，例如IFS。它不常被使用。

以下是一个示例脚本，第一次输出"Hello World"，第二次输出换行符：

\begin{lstlisting}[language=sh]
#!/bin/sh
foo="Hello World"
echo $foo
unset foo
echo $foo
\end{lstlisting}

\subsection{find}

用于搜索文件的命令非常有用，但是Linux的新手经常觉得它有点棘手，主要是因为它接受选项、测试和操作类型参数，并且一个参数的结果可以影响下一个参数。

find命令接受选项、测试和操作类型参数：
\[
\text{find [选项] [测试] [操作]}
\]

下面是find命令的一些主要组成部分：

\begin{itemize}
  \item 选项：用于设置find命令的行为。例如，-name选项用于按文件名进行匹配，-type选项用于按文件类型进行匹配等。
  \item 测试：用于指定要应用于文件的测试条件。例如，-size测试用于按文件大小进行匹配，-mtime测试用于按修改时间进行匹配等。
  \item 操作：用于指定对匹配的文件执行的操作。例如，-print操作用于将匹配的文件输出到标准输出，-delete操作用于删除匹配的文件等。
\end{itemize}

首先以root用户身份执行此命令，以确保具有搜索整个机器的权限：

\begin{lstlisting}[language=sh]
# find / -name test -print
/usr/bin/test
\end{lstlisting}

在我们的机器上运行这个命令确实需要一些时间，并且我们的Windows机器上的磁盘也会转动。这是因为我们的Linux机器通过SAMBA挂载了Windows机器文件系统的一部分。看起来好像那部分也被搜索了，尽管我们知道我们要查找的文件应该在Linux机器上。

这就是第一个选项派上用场的地方。如果您指定-mount选项，可以告诉find命令不要搜索已挂载的目录：

\begin{lstlisting}[language=sh]
# find / -mount -name test -print
/usr/bin/test
\end{lstlisting}

我们仍然可以在我们的机器上找到该文件，但这次速度更快，而且不会搜索其他已挂载的文件系统。

find命令的完整语法如下：
\[
\text{find [路径] [选项] [测试] [操作]}
\]

路径部分很简单：您可以使用绝对路径，例如/bin，或相对路径，例如.. 如果需要，还可以指定多个路径，例如find /var /home。

find命令有多个选项；下表列出了主要选项：

\begin{tabular}{|c|p{7.5cm}|}
\hline
\textbf{选项} & \textbf{含义} \\
\hline
-depth & 在查看目录本身之前先搜索目录的内容。 \\
\hline
-follow & 跟随符号链接。 \\
\hline
-maxdepth N & 最多搜索N级目录。 \\
\hline
-mount (或 -xdev) & 不搜索其他已挂载的目录。 \\
\hline
\end{tabular}

测试部分包含许多可应用于find命令的测试条件。每个测试条件都返回true或false。find命令会逐个考虑找到的每个文件，并按照定义的顺序应用每个测试条件。如果一个测试条件返回false，则find命令停止处理当前正在查看的文件，并继续下一个文件；如果一个测试条件返回true，则find命令在当前文件上处理下一个测试条件或操作。下表列出了一些常见的测试条件；请参阅手册页获取可使用find的更多可能的测试条件的详细列表。
\par %empty
\begin{tabular}{|c|p{7.5cm}|}
\hline
\textbf{测试条件} & \textbf{含义} \\
\hline
-atime N & 文件上次访问时间距今N天前。 \\
\hline
-mtime N & 文件上次修改时间距今N天前。 \\
\hline
-name 模式 & 文件名（不包括路径）与提供的模式匹配。为了确保模式传递给find命令而不是立即由shell评估，模式必须始终用引号括起来。 \\
\hline
-newer 其他文件 & 文件比其他文件更新。 \\
\hline
-type C & 文件的类型为C，其中C可以是特定的类型；最常见的是“d”表示目录和“f”表示普通文件。其他类型请参阅手册页。 \\
\hline
-user 用户名 & 文件的所有者是指定的用户名。 \\
\hline
\end{tabular}
\par %empty
您还可以使用操作符组合测试条件。大多数操作符都有两种形式：一种是短形式，一种是长形式，如下表所示：
\par %empty
\begin{tabular}{|c|c|c|}
\hline
\textbf{操作符} & \textbf{短形式} & \textbf{长形式} \\
\hline
-and & -a & -and \\
\hline
-or & -o & -or \\
\hline
-not & ! & -not \\
\hline
\end{tabular}
\par %empty

\subsection{grep}

grep是"general regular expression parser"的缩写。你可以使用find命令在系统中搜索文件，但是你可以使用grep命令在文件中搜索字符串。实际上，当使用find命令时，在-exec选项之后紧跟grep命令是非常常见的用法。

grep命令接受选项、要匹配的模式和要搜索的文件作为参数：
\begin{equation*}
    \text{grep [选项] 模式 [文件]}
\end{equation*}

如果不提供文件名，它会在标准输入中搜索。

现在让我们先看一下grep命令的主要选项。这里只列出了主要选项；请参阅手册页获取完整的选项列表。

\begin{tabular}{|c|p{7.5cm}|}
\hline
\textbf{选项} & \textbf{含义} \\
\hline
-c & 不打印匹配的行，而是打印匹配行的数量。 \\
\hline
-E & 打开扩展的正则表达式。 \\
\hline
-h & 抑制每行输出前缀中包含文件名的正常显示。 \\
\hline
-i & 忽略大小写。 \\
\hline
-l & 列出具有匹配行的文件名，而不输出实际匹配的行。 \\
\hline
-v & 反转匹配模式，选择不匹配的行而不是匹配的行。 \\
\hline
\end{tabular}

\bibliographystyle{plain}
\bibliography{crazyfish.bib}

\end{document}
